diff options
Diffstat (limited to 'src/routes/(app)/[username]/(profile)')
6 files changed, 200 insertions, 206 deletions
diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte index dfa25b78..133930b1 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.svelte +++ b/src/routes/(app)/[username]/(profile)/+layout.svelte @@ -4,7 +4,6 @@ import { Icon } from '$comp'; import { t } from '$lib/translations'; import { sign, send } from '$lib/nostr'; - import { v4 } from 'uuid'; export let data; let events, user, subject, src, text; @@ -111,7 +110,7 @@ {/if} <div class="w-full flex"> - <a href={`/${subject.pubkey}/messages`} class="mx-auto"> + <a href={`/${subject.username}/messages`} class="mx-auto"> <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60"> <div class="mx-auto flex"> <Icon icon="support" style="mr-2 my-auto" /> diff --git a/src/routes/(app)/[username]/(profile)/+page.server.js b/src/routes/(app)/[username]/(profile)/+page.server.js index c68cfecc..d6cf3984 100644 --- a/src/routes/(app)/[username]/(profile)/+page.server.js +++ b/src/routes/(app)/[username]/(profile)/+page.server.js @@ -1,22 +1,35 @@ import { auth, get } from '$lib/utils'; export async function load({ cookies, params, parent, url }) { - let { subject } = await parent(); + let { user, subject } = await parent(); let { pubkey } = subject; + let { since = 0 } = params; - let events = []; + let messages, invoices, sent, received; + messages = invoices = sent = received = []; + + if (user) { + try { + messages = await get(`/${user.pubkey}/${since}/messages`); + messages = messages.sort((a, b) => b.created_at - a.created_at); + } catch (e) { + console.log(`failed to fetch nostr messages`, e); + } + + ({ invoices, sent, received } = await get('/requests', auth(cookies))); + } + + let notes = []; try { - events = await get(`/${pubkey}/notes`); + notes = await get(`/${pubkey}/notes`); } catch (e) { - console.log(`failed to fetch nostr events for ${pubkey}`, e); + console.log(`failed to fetch nostr notes for ${pubkey}`, e); } - events.map((e) => { + notes.map((e) => { e.seen = e.created_at; }); - let { invoices, sent, received } = await get('/requests', auth(cookies)); - - return { invoices, events, sent, received }; + return { invoices, messages, notes, sent, received }; } diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte index baab8ba1..168556e4 100644 --- a/src/routes/(app)/[username]/(profile)/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/+page.svelte @@ -1,76 +1,197 @@ <script> - import { Avatar, Balance, Feed } from '$comp'; + import { onMount } from 'svelte'; + import { Icon, Avatar, Balance, Feed } from '$comp'; import { t } from '$lib/translations'; import { f, sat, sats } from '$lib/utils'; + import { sign, send, encrypt, decrypt } from '$lib/nostr'; + import { event as e, password } from '$lib/store'; + import { browser } from '$app/environment'; export let data; - let { events, invoices, sent, received, subject, user } = data; - console.log(invoices); + let { messages, notes, invoices, sent, received, subject, user } = data; + $: refresh(data); + let refresh = (d) => ({ messages, notes, invoices, sent, received, subject, user } = d); + + let keys = new Set(); + let latest = []; + let i = 0; + let ready; + + e.subscribe(async (event) => { + if (!(event && ready)) return; + if (event.recipient.id === user?.id && !~latest.findIndex((m) => m.id === event.id)) { + event.content = await decrypt({ event, user }); + + let i = latest.findIndex((m) => m.pubkey === event.pubkey); + let popped; + if (~i) popped = latest.splice(i, 1); + else popped = latest.pop(); + + latest.unshift(event); + latest = latest; + } + }); + + $: initialize($password); + let initialize = async (p) => { + ready = false; + if (!p) return; + while (latest.length < 3 && i < messages.length) { + let event = messages[i]; + let k = event.author.id === user.id ? event.recipient.pubkey : event.author.pubkey; + if (!keys.has(k)) { + keys.add(k); + event.content = await decrypt({ event, user }); + if (event.content) latest.push(event); + } + i++; + } + + latest = latest; + ready = true; + }; </script> -<div class="flex justify-center lg:justify-start"> +<div class="space-y-8"> {#if user?.id === subject.id} - <Balance {user} /> - {/if} -</div> + <div> + <div class="flex justify-center lg:justify-start"> + <Balance {user} /> + </div> + {#if !user.balance} + <div class="space-y-8 mt-8"> + <p class="text-secondary text-lg"> + Welcome! Your account balance is empty. You can start by making a deposit or by asking + someone to pay you. + </p> -<div> - {#each invoices as { amount, currency, rate, request: { requester, recipient }, hash }} - {@const requested = user.username === requester?.username} - <a href={`/${recipient.username}/invoice/${hash}` + (requested ? '/tip' : '')}> - <div class="border-b p-2 last:border-b-0 hover:bg-gray-100"> - <div class="flex gap-2"> - <div> - {#if requested} - <div class="flex"> - <Avatar user={recipient} size={20} /> - <div class="my-auto text-left"> - <p class="text-secondary">{$t('payments.invoiceFrom')}</p> - <p class="ml-1 text-lg break-words">{recipient.username}</p> + <div class="flex gap-4 justify-center w-full"> + <a href={`/${user.username}/receive`}> + <button + class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60 bg-black text-white" + > + <div class="mx-auto flex"> + <Icon icon="numpad" style="my-auto h-6 mr-2 invert" /> + <div class="my-auto mt-1">Request Payment</div> </div> + </button> + </a> + + {#if user.eligible} + <a href={`/buy`}> + <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60"> + <div class="mx-auto flex"> + <!-- <Icon icon="plus" style="my-auto h-6 mr-2" /> --> + + <img + src="/images/bitcoin.svg" + class="w-8 border-4 border-transparent mr-2" + alt="Bitcoin" + /> + <div class="my-auto mt-1">Buy Bitcoin</div> + </div> + </button> + </a> + {/if} + </div> + </div> + {/if} + </div> + {/if} + + {#if invoices.length} + <div> + <h1 class="text-secondary text-xl">Invoices</h1> + {#each invoices as { amount, currency, rate, request: { requester, recipient }, hash }} + {@const requested = user.username === requester?.username} + <a href={`/${recipient.username}/invoice/${hash}` + (requested ? '/tip' : '')}> + <div class="border-b p-2 last:border-b-0 hover:bg-gray-100"> + <div class="flex gap-2"> + <div> + {#if requested} + <div class="flex"> + <Avatar user={recipient} size={20} /> + <div class="my-auto text-left"> + <p class="text-secondary">{$t('payments.invoiceFrom')}</p> + <p class="ml-1 text-lg break-words">{recipient.username}</p> + </div> + </div> + {:else} + <div class="flex"> + <Avatar user={requester} size={20} /> + <div class="my-auto text-left"> + <p class="text-secondary">{$t('payments.awaiting')}</p> + <p class="ml-1 text-lg break-words">{requester.username}</p> + </div> + </div> + {/if} </div> - {:else} - <div class="flex"> - <Avatar user={requester} size={20} /> - <div class="my-auto text-left"> - <p class="text-secondary">{$t('payments.awaiting')}</p> - <p class="ml-1 text-lg break-words">{requester.username}</p> + <div class="whitespace-nowrap my-auto ml-auto flex gap-2"> + <div class="font-bold"> + {f(amount * (rate / sats), currency)} </div> + + <div class="text-secondary">{sat(amount)}</div> </div> - {/if} - </div> - <div class="whitespace-nowrap my-auto ml-auto flex gap-2"> - <div class="font-bold"> - {f(amount * (rate / sats), currency)} </div> - - <div class="text-secondary">{sat(amount)}</div> </div> - </div> - </div> - </a> - {/each} + </a> + {/each} + </div> + {/if} + {#if received.length} + <div> + <h1 class="text-secondary text-xl">Requests</h1> - {#each received as { id, invoice, memo, requester: r }} - <a href={`/${user.username}/receive/${id}`}> - <div class="border-b p-2 last:border-b-0 hover:bg-gray-100"> - <div class="flex"> - <div> + {#each received as { id, invoice, memo, requester: r }} + <a href={`/${user.username}/receive/${id}`}> + <div class="border-b p-2 last:border-b-0 hover:bg-gray-100"> <div class="flex"> - <Avatar user={r} size={20} /> - <div class="my-auto text-left"> - <p class="ml-1 text-lg break-words">{r.username}</p> - <p class="ml-1 text-secondary">{memo}</p> + <div> + <div class="flex"> + <Avatar user={r} size={20} /> + <div class="my-auto text-left"> + <p class="ml-1 text-lg break-words">{r.username}</p> + <p class="ml-1 text-secondary">{memo}</p> + </div> + </div> + </div> + <div class="whitespace-nowrap my-auto ml-auto flex gap-2"> + <button class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full" + >Invoice</button + > </div> </div> </div> - <div class="whitespace-nowrap my-auto ml-auto flex gap-2"> - <button class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full" - >Invoice</button - > + </a> + {/each} + </div> + {/if} + + {#if latest.length && user?.id === subject.id} + <div class="relative"> + {#each latest as { content, pubkey, author, recipient }} + {@const user = author.id === user.id ? recipient : author} + <a href={`/${user.username}/messages`}> + <div class="flex hover:bg-gray-100 p-4 rounded-2xl"> + <div class="my-auto"> + <Avatar {user} size={'20'} disabled={true} /> + </div> + <div class="my-auto truncate"> + <div class="my-auto ml-1 text-lg font-bold">{user.username}</div> + <div class="my-auto ml-1 text-secondary text-lg truncate"> + {content} + </div> + </div> </div> - </div> - </div> - </a> - {/each} + </a> + {/each} + </div> + {/if} </div> + +<style> + h1 { + @apply mb-2; + } +</style> diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.server.js b/src/routes/(app)/[username]/(profile)/messages/+page.server.js deleted file mode 100644 index 63116099..00000000 --- a/src/routes/(app)/[username]/(profile)/messages/+page.server.js +++ /dev/null @@ -1,16 +0,0 @@ -import { get } from '$lib/utils'; - -export async function load({ params, parent, url }) { - let { user, subject } = await parent(); - let { since = 0 } = params; - - let messages = []; - - try { - messages = await get(`/nostr/${user.pubkey}/${subject.pubkey}/${since}/messages`); - } catch (e) { - console.log(`failed to fetch nostr messages`, e); - } - - return { newMessages: messages }; -} diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.svelte b/src/routes/(app)/[username]/(profile)/messages/+page.svelte deleted file mode 100644 index db7b1ced..00000000 --- a/src/routes/(app)/[username]/(profile)/messages/+page.svelte +++ /dev/null @@ -1,123 +0,0 @@ -<script> - import { browser } from '$app/environment'; - import { t } from '$lib/translations'; - import { format, parseISO } from 'date-fns'; - import { scale } from 'svelte/transition'; - import { enhance } from '$app/forms'; - import { Icon } from '$comp'; - import { back, fail, focus } from '$lib/utils'; - import { sign, send, encrypt, decrypt } from '$lib/nostr'; - import { messages, password } from '$lib/store'; - import { tick, onMount } from 'svelte'; - import { calculateId } from 'nostr'; - - export let data; - - let { newMessages, subject, user } = data; - let input, pane; - - $: initialize($password); - let initialize = async (p) => { - if (!p) return; - $messages[user.pubkey] = $messages[user.pubkey] || {}; - $messages[user.pubkey][subject.pubkey] = $messages[user.pubkey][subject.pubkey] || {}; - - for (let m of newMessages) { - if (!$messages[user.pubkey][subject.pubkey][m.id]) { - m.message = await decrypt({ event: m, user }); - $messages[user.pubkey][subject.pubkey][m.id] = m; - tick().then(() => pane && (pane.scrollTop = pane.scrollHeight)); - } - } - tick().then(() => pane && (pane.scrollTop = pane.scrollHeight)); - }; - - $: events = - ($messages[user.pubkey] && - $messages[user.pubkey][subject.pubkey] && - Object.values($messages[user.pubkey][subject.pubkey]).sort( - (a, b) => a.created_at - b.created - )) || - []; - - let sent, submitting, message; - let submit = async () => { - submitting = true; - - let event = { - pubkey: user.pubkey, - created_at: Math.floor(Date.now() / 1000), - kind: 4, - tags: [['p', subject.pubkey]] - }; - - event.message = message; - event.id = await calculateId(event); - $messages[user.pubkey][subject.pubkey][event.id] = event; - $messages = $messages; - tick().then(() => pane && (pane.scrollTop = pane.scrollHeight)); - - try { - event.content = await encrypt({ message, recipient: subject.pubkey, user }); - await sign({ event, user }); - await send(event); - - sent = true; - } catch (e) { - fail('Failed to send message'); - delete $messages[user.pubkey][subject.pubkey][event.id]; - $messages = $messages; - } - - message = ''; - submitting = false; - }; -</script> - -<div class="container max-w-lg px-4 mx-auto mt-10 space-y-5"> - <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> - {$t('payments.sendMessage')} - </h1> - - <div class="max-h-[calc(50vh)] overflow-y-scroll p-4" bind:this={pane}> - {#each events as data} - {@const ours = data.pubkey === user.pubkey} - {@const theirs = !ours} - <div class="rounded-2xl p-4 max-w-[300px] mb-1 text-md" class:ours class:theirs> - {#if data.message} - {data.message} - {:else} - - - {/if} - </div> - <div class="text-sm text-gray-400 mb-6" class:text-right={ours}> - {format(new Date(data.created_at * 1000), 'MMM d')}, - {format(new Date(data.created_at * 1000), 'h:mm aa')} - </div> - {/each} - </div> - - <form method="POST" class="space-y-5" on:submit|preventDefault={submit}> - <input type="hidden" name="requester_id" value={user.id} /> - <input type="hidden" name="recipient" value={subject.username} /> - - <input - use:focus - name="message" - placeholder={$t('payments.sendMessage')} - class="w-full p-4 border rounded-xl" - bind:value={message} - bind:this={input} - /> - </form> -</div> - -<style> - .ours { - @apply bg-gray-100 rounded-br-none ml-auto; - } - - .theirs { - @apply bg-gray-200 rounded-bl-none; - } -</style> diff --git a/src/routes/(app)/[username]/(profile)/request/+page.server.js b/src/routes/(app)/[username]/(profile)/request/+page.server.js index 2c1d6bde..e7ad6a8f 100644 --- a/src/routes/(app)/[username]/(profile)/request/+page.server.js +++ b/src/routes/(app)/[username]/(profile)/request/+page.server.js @@ -1,15 +1,15 @@ import { redirect } from '@sveltejs/kit'; -import { auth, post } from '$lib/utils'; +import { fd, auth, post } from '$lib/utils'; export const actions = { create: async ({ cookies, params, request, url }) => { - let form = Object.fromEntries(await request.formData()); + let form = await fd(request); let { id, requester } = await post('/requests', form, auth(cookies)); throw redirect(307, `/${params.username}/request/${id}`); }, delete: async ({ cookies, request }) => { - let form = Object.fromEntries(await request.formData()); + let form = await fd(request); let r = await post('/requests/delete', form, auth(cookies)); } }; |
